前面博客中使用了内存缓存,我们先来回答一个问题:为什么要使用缓存?
移动设备在使用网络时往往面临一个问题,那就是流量是需要收费的,这就需要软件开发者在开发时应当尽量避免流量的消耗,而流量消耗的大头就是图片。这时候本地缓存就是一个很好的解决方式。而且移动设备用户所处的网络环境也是不可知的,如果用户处于弱网络环境下,那图片加载所要消耗的时间将是不可忍受的,这也是使用本地缓存的原因之一。
同时,移动设备的内存是有限的,如果一个应用包含大量的图片,全部放到内存中必然会触发 OOM,可如果每次都要重新从本次磁盘加载的话,性能就会有很大的消耗。而且本地加载虽然比网络要快但也是需要时间的,这也往往造成界面的卡顿。这时候一个好的内存缓存策略就是不可或缺的。
明白了为什么使用,接下来就要考虑怎么实现了。大家先看一张图:
首先,程序会在内存缓存中查找 Bitmap,如果命中则直接显示,如果没有就会去本地磁盘缓存中查找缓存文件;如果在磁盘缓存中命中就将缓存文件转换为 Bitmap 再进行显示,这个过程中会将 Bitmap 加入内存缓存中;如果本地磁盘中没有就会从网络上进行下载,并且缓存在磁盘和内存中。
现在我们先来实现磁盘缓存(DiskCache):
然后修改 ImageLoader.java 源码进行测试,可使用:
接下来要实现的是:首先使用内存缓存,如果内存缓存没有图片再使用 SD 卡缓存,如果 SD 卡中也没有图片,最后才从网络上获取。于是新建一个双缓存类 DoubleCache.java,源码如下:
很快就发现,ImageCache、DiskCache 和 DoubleCache,有共同的行为,那就是对 Bitmap 的存取操作,只是具体的实现细节不同。此处,我们可以对他们进行抽象出共同的接口 IBitmapCache,有 get、put 和 remove 方法。对于数据源,一般都是提供增删改查操作。此时 ImageCache 类名改为 MemoryCache 更恰当。
IBitmapCache.java
MemoryCache.java
DoubleCache.java
DiskCache.java
接下来重构 ImageLoader 类:
ImageLoaderConfig 类修改同上,略。
经过此次重构,用户可以通过 setCache(IBitmapCache cache) 函数设置缓存实现,也就是通常说的依赖注入。具体如下所示:
在上述代码中,通过 setCache(IBitmapCache cache) 方法注入不同的缓存实现,这样不仅能够使 ImageLoader 更简单、健壮,也使得 ImageLoader 的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache 缓存图片的具体实现完全不一样,但是,它们的一个特点是,都实现了 IBitmapCache 接口。当用户需要自定义实现缓存策略时,只需要新建一个实现 IBitmapCache 接口的类,然后构造该类的对象,并且通过 setCache 函数注入到 ImageLoaderConfig 中,这样 ImageLoader 就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致 ImageLoader 类的修改。
接下来,我们再新增一个缓存类 NoCache:
然后就没有然后了,因为不需要修改其他模块的任何代码,这就是抽象的魅力,而且几乎符合六大原则:功能单一,满足单一职责原则;对扩展开放对修改关闭,满足开闭原则;setBitmapCache(IBitmapCache cache) 的用法同时满足里氏替换原则和依赖倒置原则;IBitmapCache 接口类几乎不可拆分,满足接口隔离原则;对于 ImageLoader 来说最直接的朋友是 IBitmapCache,至于你依赖 LruCache 对象还是 File 文件系统,都与我无关,符合迪米特原则。
在上面代码中,内存缓存我们使用了官方的 LruCache 类,对于磁盘缓存,我们选择 Jake Wharton 大神的 DiskLruCache 类。先来看看 DiskCache 改造后的代码:
其中有些方法已经放到其他工具类中,如 Md5Helper、IOUtil;有些没有,如 getDiskCacheDir()、getAppVersion() 等,这些细节暂不表。此处用了单例模式,没有持有 Context 对象,防止内存泄漏。如果必须持有 Context 对象,建议使用下面方式:
因为创建 DiskCache 需要 Context 对象,对它依赖的对象都要修改代码。在本项目中只有 DoubleCache 中持有磁盘缓存对象,所以修改代码不多。
另附工具类代码 IOUtil.java
Md5Helper.java: